/**
 * Copyright (c) 2010-2011 Pierre Labatut
 *
 * This file is part of Space-Chronicles project.
 *
 * Space-Chronicles is free software: you can redistribute it and/or modify it
 * under the terms of its license which may be found in the accompanying
 * LICENSE file or <http://code.google.com/p/space-chronicles/>.
 */

package space.stream;

import java.io.DataInput;
import java.io.DataInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.Array;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.net.URL;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.ServiceLoader;

/**
 *
 * @author Pierre
 */
public class StructureInput {

    final static Map<Class<?>, StructureIO<?>> readers;

    static {
        final Map<Class<?>, StructureIO<?>> map = new HashMap<Class<?>, StructureIO<?>>();
        for (StructureIO r : ServiceLoader.load(StructureIO.class)) {
            map.put(r.type(), r);
        }
        /**
         * Builtin
         */
        {
            IOMatrix4 iOMatrix4 = new IOMatrix4();
            map.put(iOMatrix4.type(), iOMatrix4);
        }
        {
            IOVector2f iOVector2f = new IOVector2f();
            map.put(iOVector2f.type(), iOVector2f);
        }
        {
            IOVector3 iOVector3 = new IOVector3();
            map.put(iOVector3.type(), iOVector3);
        }
        {
            IOVector3f iOVector3f = new IOVector3f();
            map.put(iOVector3f.type(), iOVector3f);
        }
        readers = Collections.unmodifiableMap(map);
    }
    final DataInput di;

    public StructureInput(InputStream is) {
        di = new DataInputStream(is);
    }

    public int readInt() {
        try {
            return di.readInt();
        } catch (IOException ex) {
            throw new IllegalStateException(ex);
        }
    }

    public float readFloat() {
        try {
            return di.readFloat();
        } catch (IOException ex) {
            throw new IllegalStateException(ex);
        }
    }

    public float[] readFloatArray() {
        final int length = readInt();
        final float[] array = new float[length];
        for (int i = 0; i != length; i++) {
            array[i] = readFloat();
        }
        return array;
    }

    public int[] readIntArray() {
        final int length = readInt();
        final int[] array = new int[length];
        for (int i = 0; i != length; i++) {
            array[i] = readInt();
        }
        return array;
    }

    public short readShort() {
        try {
            return di.readShort();
        } catch (IOException ex) {
            throw new IllegalStateException(ex);
        }
    }

    public short[] readShortArray() {
        final int length = readInt();
        final short[] array = new short[length];
        for (int i = 0; i != length; i++) {
            array[i] = readShort();
        }
        return array;
    }

    public void readFully(byte[] b) {
        try {
            di.readFully(b);
        } catch (IOException ex) {
            throw new IllegalStateException(ex);
        }
    }

    public ByteBuffer readByteBuffer() {
        final int length = readInt();
        final ByteBuffer buffer = ByteBuffer.allocateDirect(length);
        buffer.order(ByteOrder.nativeOrder());
        final byte[] array = new byte[length];
        readFully(array);
        buffer.put(array);
        buffer.rewind();
        return buffer;
    }

    public String readString() {
        try {
            return di.readUTF();
        } catch (IOException ex) {
            throw new IllegalStateException(ex);
        }
    }

    public double readDouble() {
        try {
            return di.readDouble();
        } catch (IOException ex) {
            throw new IllegalStateException(ex);
        }
    }

    public <T> T[] readArray(Class<T> objectBuilder) {
        int length = readInt();
        final T[] array = (T[]) Array.newInstance(objectBuilder, length);
        for (int i = 0; i < length; i++) {
            array[i] = readObject(objectBuilder);
        }
        return array;
    }

    public <T> T[] readArray(Class<T> arrayType, Class<T>[] objectClass) {
        int length = readInt();
        final T[] array = (T[]) Array.newInstance(arrayType, length);
        for (int i = 0; i < length; i++) {
            array[i] = readObject(objectClass);
        }
        return array;
    }

    public <T> T readObject(Class<T>[] objectClass) {
        final String className = readString();
        for (Class<T> clazz : objectClass) {
            if (className.equals(clazz.getSimpleName())) {
                return readObject(clazz);
            }
        }
        throw new RuntimeException("Unexpected class name :" + className);
    }

    /**
     * Uses reflexivity to run the class constructor and pass this StructureInput
     * as argument
     * @param <T> the type of object to be read
     * @param objectClass the type of object to be read
     * @return an instance of hte object initialized with data read from this StructureInput
     * @throws IOException
     */
    public <T> T readObject(Class<T> objectClass) {
        /**
         * Try to fin a reader amon the declared ones
         */
        final StructureIO<?> readObject = readers.get(objectClass);
        if (readObject != null) {
            return (T) readObject.read(this);
        }

        try {
            final Constructor<T> ctor = objectClass.getConstructor(getClass());
            return ctor.newInstance(this);
        } catch (InstantiationException ex) {
            throw new RuntimeException(
                    "Unable to instanciate the class " + objectClass.getName() + ":"
                    + "the class object represents an abstract class, an interface, an array class, a primitive type, or void",
                    ex);
        } catch (IllegalAccessException ex) {
            throw new RuntimeException("Unable to instanciate the class " + objectClass.getName() + ":"
                    + "the constructor or the class is not accessible", ex);
        } catch (IllegalArgumentException ex) {
            throw new RuntimeException("Unable to instanciate the class " + objectClass.getName() + ":"
                    + "the argument passed to the constructor is incorrect", ex);
        } catch (InvocationTargetException ex) {
            if (ex.getCause() instanceof RuntimeException) {
                throw (RuntimeException) ex.getCause();
            } else {
                throw new RuntimeException("Unable to instanciate the class " + objectClass.getName() + ":"
                        + "the constructor thrown an exception", ex.getCause());
            }
        } catch (NoSuchMethodException ex) {
            throw new RuntimeException(
                    "Unable to instanciate the class " + objectClass.getName() + ":"
                    + "No constructor accept " + getClass().getName() + " as only argument",
                    ex);
        } catch (SecurityException ex) {
            throw new RuntimeException("Unable to instanciate the class " + objectClass.getName() + ":"
                    + "Reflexivity is forbidden by the security manager", ex);
        }
    }

    private static <T> T readObject(final InputStream is, final Class<T> objectClass) {
        try {
            final StructureInput si = new StructureInput(is);
            return si.readObject(objectClass);
        } finally {
            try {
                is.close();
            } catch (IOException ex) {
                throw new RuntimeException(ex);
            }
        }
    }

    public static <T> T readObject(URL libUrl, Class<T> aClass) {
        if (libUrl== null) {
            throw new IllegalArgumentException();
        }
        try {
            final InputStream is = libUrl.openStream();
            try {
                return readObject(is, aClass);
            } finally {
                is.close();
            }
        } catch (IOException ex) {
            throw new RuntimeException(ex);
        }
    }
}
